import copy
import random
from logging import Logger

from simpy import Environment, Resource

from task_service.task_context import StorerDeliverLayer
from util.dateutil import ts_str
from x_pc.core import System
from x_pc.core.system_context import SysDataCtx
from x_pc.core.unit import Unit
from x_pc.db.sf_data import Sku
from x_pc.helpers import can_work_max_time
from x_pc.model.model import TaskCtx, check_fix_task, TaskStatus, PackType, Task, TaskSource


class UnitDevice(Unit):
    """
    工作单元设备
    """
    resource: Resource = None

    def __init__(self, env: Environment, name: str, kwargs):
        super().__init__(env, name, kwargs)
        self.resource = Resource(env)

    def device_type(self):
        return self.equ_type


class UnitGroup(System):
    """
    工作单元 集合(组)
    """
    ware_house = None

    device_type: int = None
    device_list: list[UnitDevice] = []

    # 存储 待交付对象 及PCS数
    factory_port_dict = {}
    # 存储 交付对象 及可用口
    factory_port = {}

    # 存储 待交付对象 及PCS数
    zk_factory_port_dict = {}
    # 存储 交付对象 及可用口
    zk_factory_port = {}

    # 系统数据上下文
    sys_data: SysDataCtx = None

    def __init__(self, env: Environment, name: str, device_type: int, ware_house, data_list: list[UnitDevice],
                 sys_data: SysDataCtx):
        super().__init__(env, name, None)
        self.ware_house = ware_house
        self.device_type = device_type
        self.device_list = data_list
        self.sys_data = sys_data
        # 在库
        if self.device_type in sys_data.zk_ports:
            self.__init_zk_port_zone()
        # 发货
        if self.device_type in sys_data.ch_ports:
            self.__init_port_zone()

    def __init_port_zone(self):
        """
        初始化 发货出货口 分配
        :return:
        """
        factory_port_dict = self.sys_data.factory_port_dict
        # 副本
        self.factory_port_dict = copy.deepcopy(factory_port_dict)
        # 初始化 数据
        self.factory_port[self.device_type] = {}

        # 出货量统计数据
        for d in self.device_list:
            if d.device_code() not in self.sys_data.statistical_factory_port[self.device_type]:
                self.sys_data.statistical_factory_port[self.device_type][d.device_code()] = 0
        self.sys_data.logger.debug('[PC] 历史出货口出货量 统计完善 (%s): %s', self.device_type,
                                   self.sys_data.statistical_factory_port[self.device_type])

        # 出货口分配
        for f in factory_port_dict[self.device_type]:
            self.factory_port[self.device_type][f] = []
        # 分配
        prev_factory_port = self.sys_data.prev_factory_port[
            self.device_type] if self.device_type in self.sys_data.prev_factory_port else {}
        factory_port = self.factory_port[self.device_type] if self.device_type in self.factory_port else {}
        if self.assign_port(self.device_list, self.factory_port_dict[self.device_type], factory_port,
                            prev_factory_port, self.sys_data.store_layer_dict, self.sys_data.is_designate_outlet,
                            self.sys_data.statistical_factory_port[self.device_type],
                            self.sys_data.urgent_order_add_date_dict, self.device_list, self.sys_data.logger):
            # 回写 口分配
            self.sys_data.factory_port[self.device_type] = self.factory_port[self.device_type]
        # 日志
        msg = (
            f'[PC] 交付口({self.device_type}) 初始分配 '
            f'\n 可用口数量: {len(self.device_list)} -> {self.device_list} '
            f'\n 交付对象: {len(self.factory_port_dict[self.device_type])} -> {self.factory_port_dict[self.device_type]}'
            f'\n 交付口分配(初始): {self.factory_port[self.device_type]}'
        )
        self.sys_data.logger.info(msg)

    def __init_zk_port_zone(self):
        """
        初始化 在库任务 出货口 分配逻辑
        """
        zk_factory_port_dict = self.sys_data.zk_factory_port_dict
        # 副本
        self.zk_factory_port_dict = copy.deepcopy(zk_factory_port_dict)
        # 初始化数据
        self.zk_factory_port[self.device_type] = {}
        for fp in zk_factory_port_dict[self.device_type].values():
            for order_id in fp.keys():
                self.zk_factory_port[self.device_type][order_id] = []
        #
        if self.assign_port_zk(self.device_list, self.zk_factory_port_dict[self.device_type],
                               self.zk_factory_port[self.device_type],
                               self.sys_data.zk_prev_factory_port[self.device_type], self.sys_data.logger):
            # 回写
            self.sys_data.zk_factory_port[self.device_type] = self.zk_factory_port[self.device_type]
        # 日志
        msg = (
            f'[PC] 交付口 (在库-{self.device_type}) 初始分配'
            f'\n 交付订单: {self.zk_factory_port_dict[self.device_type]}'
            f'\n 交付口分配(初次): {self.zk_factory_port[self.device_type]}'
        )
        self.sys_data.logger.info(msg)

    @staticmethod
    def assign_port_zk(port_list, zk_factory_port_dict, zk_factory_port, zk_pre_factory_port, logger: Logger):
        if not zk_factory_port_dict or not zk_factory_port:
            return False
        assigned = False
        copied_port_list = copy.copy(port_list)
        # 先看下是否有需要出货口延续的
        if zk_pre_factory_port:
            factory_infos = dict()
            for pd in zk_factory_port_dict.values():
                for order_id in pd:
                    factory_infos[order_id] = pd[order_id]
            zk_factory_port_dict.values()
            for fac in zk_factory_port:
                # 若已分配 或 无PCS数，则跳过
                if zk_factory_port[fac] or factory_infos[fac]['pcs'] <= 0:
                    continue
                if fac in zk_pre_factory_port:
                    # 获取 上一次分配的出货口
                    pre_ports = zk_pre_factory_port[fac]
                    if not pre_ports:
                        continue
                    # 看下当前可用的出货口
                    pre_port = [_ for _ in copied_port_list if _.equ_code == pre_ports[0]]
                    if pre_port:
                        assigned = True
                        zk_factory_port[fac] = pre_port
                        copied_port_list.remove(pre_port[0])
                if not copied_port_list:
                    break

        # 再看下是否有指定出口的
        for port_equ, sub_order_dict in zk_factory_port_dict.items():
            if not port_equ:
                continue
            port_code_list = [_.equ_code for _ in copied_port_list]
            if not port_code_list:
                break
            # 该设备有 相应的出货口，则尝试分配
            if port_equ in port_code_list:
                # 对该组 进行分配  # XXX: 可进行分配优先级排序
                for order_id, infos in sub_order_dict.items():
                    if infos['pcs'] > 0:
                        # 分配
                        zk_factory_port[order_id] = [p for p in copied_port_list if p.equ_code == port_equ]
                        if zk_factory_port[order_id]:
                            assigned = True
                            copied_port_list.remove(zk_factory_port[order_id][0])

        # 若还有剩余的出货口，则分配给未指定出货口的
        for port_equ, sub_order_dict in zk_factory_port_dict.items():
            if port_equ:
                continue
            if not copied_port_list:
                break
            for order_id, infos in sub_order_dict.items():
                # 当有货物 且 未分配出货口
                if infos['pcs'] > 0 and not zk_factory_port[order_id]:
                    zk_factory_port[order_id] = [copied_port_list.pop()]
                    assigned = True
                if not copied_port_list:
                    break
        return assigned

    @staticmethod
    def choose_port(port_list: list[UnitDevice], statistical_port: dict[str, int],
                    equid_list: list[str]) -> UnitDevice | None:
        """
        根据 出货量 选择一个出货口
        :param port_list: 所有的出货口列表
        :param statistical_port: 设备统计静态信息
        :param equid_list: 当前任务所有可用的设备IDs
        """
        if equid_list:
            for c in sorted(statistical_port.items(), key=lambda _: _[1]):
                ps = [_ for _ in port_list if _.device_code() == c[0] and _.id in equid_list]
                if ps:
                    return ps[0]
            # 当匹配不到指定交付口时，暂时不分配
            return None
        for c in sorted(statistical_port.items(), key=lambda _: _[1]):
            ps = [_ for _ in port_list if _.device_code() == c[0]]
            if ps:
                return ps[0]
        return random.choice(port_list) if port_list else None

    @staticmethod
    def assign_port(port_list, factory_port_dict, factory_port, pre_factory_port,
                    store_layer_dict: dict[str, StorerDeliverLayer], is_designate_outlet: bool, statistical_port,
                    urgent_add_date_dict, device_list: list[UnitDevice], logger: Logger) -> bool:
        """
        分配出口 尽量按发车时间 顺序 分口
        :param port_list:
        :param factory_port_dict:
        :param factory_port:
        :param pre_factory_port:
        :param store_layer_dict: 交付楼层配置 及 指定交付口
        :param is_designate_outlet: 是否指定出货口
        :param statistical_port:
        :param urgent_add_date_dict:
        :param device_list: 全部可用设备列表
        :param logger:
        :return:
        """
        copied_port_list = copy.copy(port_list)
        if not copied_port_list:
            return False
        equ_type = copied_port_list[0].equ_type
        # 仅处理 PCS>0
        factory_port_items = [_ for _ in list(factory_port_dict.items()) if _[1]['pcs'] > 0]
        if not factory_port_items:
            return False
        # 分配计数
        assigned_ports_nums = 0

        # ----------------------- 指定交付口
        if is_designate_outlet:
            # 排除急料
            designate_factory_port_items = factory_port_items
            if designate_factory_port_items:
                # [排序] 按发车时间
                designate_factory_port_items.sort(key=lambda _: _[1]['vehicle_begin'])
                for dfp in designate_factory_port_items:
                    raw_factory_code = dfp[0]
                    factory_code = dfp[0]
                    if dfp[1]['urgent']:
                        factory_code = factory_code.removeprefix('urgent_')
                    pcs = dfp[1]['pcs']
                    if factory_code in store_layer_dict and pcs > 0:
                        port_nums = dfp[1]['port_nums']
                        # 如果当前已经分配了足够的口，则跳过
                        if len(factory_port[raw_factory_code]) >= port_nums:
                            continue
                        store = store_layer_dict[factory_code]
                        designate_outlets = store.designate_outlets
                        # 不夸楼层场景 指定出货口分配
                        if store.designate_equtype and store.designate_equtype == equ_type and designate_outlets:
                            designate_nums = len(designate_outlets)
                            # 若 指定数量 > 时间窗分配数量
                            if designate_nums > port_nums:
                                for idx in range(port_nums):
                                    port = UnitGroup.choose_port(copied_port_list, statistical_port, designate_outlets)
                                    if not port:
                                        break
                                    factory_port[raw_factory_code].append(port)
                                    assigned_ports_nums += 1
                                    copied_port_list.remove(port)
                            # 若 指定数量 <= 时间窗分配数量
                            else:
                                # 先按指定分配口 进行分配
                                assigned_ports = []
                                for cp in copied_port_list:
                                    if cp.device_code() in designate_outlets:
                                        assigned_ports.append(cp)
                                        assigned_ports_nums += 1
                                        factory_port[raw_factory_code].append(cp)
                                copied_port_list = [_ for _ in copied_port_list if _ not in assigned_ports]
                                # 还需额外分配的出口数量
                                ext_port_nums = port_nums - len(designate_outlets)
                                if ext_port_nums > 0 and copied_port_list:
                                    for _ in range(ext_port_nums):
                                        port = UnitGroup.choose_port(copied_port_list, statistical_port, [])
                                        if not port:
                                            break
                                        factory_port[raw_factory_code].append(port)
                                        assigned_ports_nums += 1
                                        copied_port_list.remove(port)
        # 需要指定交付口的场景
        designate_factories = []
        for fcp in factory_port_items:
            factory_code = fcp[0]
            if fcp[1]['urgent']:
                factory_code = factory_code.removeprefix('urgent_')
            if factory_code in store_layer_dict and store_layer_dict[factory_code].designate_equtype and \
                    store_layer_dict[factory_code].designate_equtype == equ_type and store_layer_dict[
                factory_code].designate_outlets:
                designate_factories.append(factory_code)

        # ----------------------- Urgent急料 (排除指定交付口的那部分急料)
        urgent_factory_port_items = [_ for _ in factory_port_items if
                                     _[1]['urgent'] and _[0] not in designate_factories]
        if urgent_factory_port_items:
            # 按 订单添加顺序 排序
            urgent_factory_port_items.sort(
                key=lambda _: urgent_add_date_dict[_[0]] if _[0] in urgent_add_date_dict else 0)
            for ufp in urgent_factory_port_items:
                factory_code = ufp[0]
                pcs = ufp[1]['pcs']
                port_nums = ufp[1]['port_nums']
                if pcs <= 0:
                    continue
                fix_factory_code = factory_code.removeprefix('urgent_')
                # 预排场景
                if pre_factory_port and fix_factory_code in pre_factory_port:
                    pre_assigned_ports = pre_factory_port[fix_factory_code]
                    assigned_ports = []
                    for port in copied_port_list:
                        if port.device_code() in pre_assigned_ports and len(factory_port[factory_code]) < port_nums:
                            factory_port[factory_code].append(port)
                            assigned_ports.append(port)
                            assigned_ports_nums += 1
                    if assigned_ports:
                        copied_port_list = [_ for _ in copied_port_list if _ not in assigned_ports]
            # 正常分配场景
            assigned_ports = []
            for ufp in urgent_factory_port_items:
                factory_code = ufp[0]
                pcs = ufp[1]['pcs']
                port_nums = ufp[1]['port_nums']
                equid_list = ufp[1]['equid_list'] if 'equid_list' in ufp[1] else []
                if pcs <= 0:
                    continue
                if len(factory_port[factory_code]) < port_nums:
                    port = UnitGroup.choose_port(copied_port_list, statistical_port, equid_list)
                    if not port:
                        continue
                    factory_port[factory_code].append(port)
                    assigned_ports.append(port)
                    assigned_ports_nums += 1
                if assigned_ports:
                    copied_port_list = [_ for _ in copied_port_list if _ not in assigned_ports]

        # 当没有交付对象 或者 出货口时，退出
        factory_port_items = [_ for _ in factory_port_items if not _[1]['urgent']]
        if not factory_port_items or not copied_port_list:
            return assigned_ports_nums > 0
        factory_port_items.sort(key=lambda _: _[1]['vehicle_begin'])

        # ----------------------- 预分配场景
        # 预先分配的交付对象Code
        all_device_codes = [_.device_code() for _ in device_list]
        pre_factory_codes = set()
        for fp in factory_port_items:
            factory_code = fp[0]
            # 说明 有预分配(历史出货口)
            if factory_code in pre_factory_port:
                pre_assigned_ports = pre_factory_port[factory_code]
                # 还需校验一下，历史出货口是否可用
                filtered_pre_assigned_ports = [p for p in pre_assigned_ports if p in all_device_codes]
                if filtered_pre_assigned_ports:
                    pre_factory_port[factory_code] = filtered_pre_assigned_ports
                    # 记录下
                    pre_factory_codes.add(factory_code)

        # 优先处理需要锁定分配口的场景
        assigned_ports = []
        if pre_factory_port and copied_port_list and pre_factory_codes:
            for port in copied_port_list:
                for fp in factory_port_items:
                    factory_code = fp[0]
                    # 无需预分配
                    if factory_code not in pre_factory_codes:
                        continue
                    # 需要指定分配
                    if factory_code in designate_factories:
                        continue
                    pcs = fp[1]['pcs']
                    port_nums = fp[1]['port_nums']
                    if pcs > 0 and factory_code in pre_factory_port:
                        pre_assigned_ports = pre_factory_port[factory_code]
                        if port.device_code() in pre_assigned_ports and len(factory_port[factory_code]) < port_nums:
                            factory_port[factory_code].append(port)
                            assigned_ports.append(port)
                            assigned_ports_nums += 1
                            break
            if assigned_ports:
                copied_port_list = [_ for _ in copied_port_list if _ not in assigned_ports]

        # ----------------------- 常规分配逻辑
        # 后续 根据 交付对象 预分配的 出货口数量 进行分配
        if copied_port_list:
            for fp in factory_port_items:
                factory_code = fp[0]
                # 此处不再处理 预设定(历史)出货口场景
                if factory_code in pre_factory_codes:
                    continue
                # 此处不再处理 需要指定分配的场景
                if factory_code in designate_factories:
                    continue
                pcs = fp[1]['pcs']
                port_nums = fp[1]['port_nums']
                equid_list = fp[1]['equid_list'] if 'equid_list' in fp[1] else []
                while pcs > 0 and len(factory_port[factory_code]) < port_nums:
                    port = UnitGroup.choose_port(copied_port_list, statistical_port, equid_list)
                    if not port:
                        break
                    factory_port[factory_code].append(port)
                    assigned_ports_nums += 1
                    copied_port_list = [_ for _ in copied_port_list if _ is not port]
                    if not copied_port_list:
                        return assigned_ports_nums > 0
        return assigned_ports_nums > 0

    def run_task(self, task_ctx: TaskCtx):
        task = task_ctx.task
        process = task.process
        available_devices = [_ for _ in self.device_list if _.id in process.ext_step.ext_equid_list]
        # 设备工作类型限制 (仅当有可用设备时 才进行过滤)
        if available_devices:
            ex_d = available_devices[0]
            equ_type = str(ex_d.equ_type)
            if equ_type in self.sys_data.work_station_dict:
                command_code = task.ext_route.command_type_code
                if command_code in self.sys_data.work_station_dict[equ_type]:
                    available_work_devices_code = self.sys_data.work_station_dict[equ_type][command_code]
                    filtered_available_devices = [ad for ad in available_devices if
                                                  ad.device_code() in available_work_devices_code]
                    self.sys_data.logger.debug(
                        f'[PC] 根据工作类型过滤可用设备 (Unit) '
                        f'task： {task.task_id} ({command_code}), '
                        f'equ_type: {equ_type}, '
                        f'before: {[_.equ_code for _ in available_devices]},'
                        f'after: {[_.equ_code for _ in filtered_available_devices]}')
                    available_devices = filtered_available_devices
                else:
                    self.sys_data.logger.debug(
                        f'[PC] 根据工作类型过滤可用设备 (Unit) - 未配置workType'
                        f'task： {task.task_id} ({command_code}), '
                        f'equ_type: {equ_type}, '
                        f'before: {[_.equ_code for _ in available_devices]},'
                        f'after: {[]}')
                    available_devices = []
                    task.ext_last_info = f'Unit {equ_type} (workType) 未配置该类型workType, 无可用设备'
        if not available_devices:
            self.sys_data.logger.error(
                f'[PC] Unit 缺少该类型设备数据 任务中止 task: {task.task_id}, equ_type: {process.ext_step.ext_equ_type}')
            task.set_run_status(TaskStatus.ERROR, self.sys_data)
            task.ext_last_info = f'Unit 缺少该类型设备数据 任务中止 {task.task_id}, equ_type: {process.ext_step.ext_equ_type}'
            return
        # [交付口隔离] 需进行交付口隔离
        if self.device_type in self.sys_data.ch_ports and task.ext_source == TaskSource.DELIVERY:
            # 选择 可选的出口
            available_devices = self.factory_port[self.device_type][task.factory_code()]
            # 能执行到这一步，说明肯定有未锁定的口
            unlocked_devices = []
            for d in available_devices:
                pending_list = self.sys_data.get_pending_device(d.device_type(), d.device_code())
                if can_work_max_time(self.env.now, 0, pending_list):
                    unlocked_devices.append(d)
            self.logger.debug(
                f'[PC] 出货口({self.device_type}) task: {task.task_id} 未锁定数: {[_ for _ in unlocked_devices]}, 总数: {[_ for _ in available_devices]}')
            # 随机选择一个
            device: UnitDevice = random.choice(unlocked_devices) if unlocked_devices else None
        # 在库任务
        elif task.ext_route.command_type_code in self.sys_data.inv_isolated_task and self.device_type in self.sys_data.zk_ports:
            available_devices = self.zk_factory_port[self.device_type][task.cwmsinvtaskdetil]
            device = random.choice(available_devices)
            msg = f'[PC] 在库 指定交付口 task: {task.task_id}, port: {device.device_code()}'
            self.logger.debug(msg)
        else:
            device: UnitDevice = random.choice(available_devices) if available_devices else None
        if not device:
            self.sys_data.logger.error(
                f'[PC] 设备缺失 任务中止 task: {task.task_id}, route: {task.ext_route.id}({task.ext_route.name}), step: {process.ext_step.id}, equid_list: {process.ext_step.ext_equid_list}')
            task.set_run_status(TaskStatus.ERROR, self.sys_data)
            task.ext_last_info = f'设备缺失 route: {task.ext_route.id}({task.ext_route.name}), step: {process.ext_step.id}, device_type: {self.device_type}'
            return
        with device.resource.request() as req:
            yield req
            # 若上一个任务为运动节点，设置其终点为本设备
            if task.pre_process().ext_resource_type.is_movable():
                task.pre_process().end_pos = device.loc()
            #
            process.begin_ts = self.env.now
            process.begin_pos = device.loc()
            process.set_origin_pack_type(task.pre_process().target_pack_type)
            process.set_origin_sku_num(task.pre_process().target_sku_num)
            process.operate_pack_type = process.ext_work_type.operate_pack_type
            process.operate_pack_num = 1
            # 当指定出货口时，若为其它层出口，直接替换
            if self.sys_data.is_designate_outlet:
                if device.device_type() == task.ext_route.ext_ch_port:
                    if task.ext_designate_equtype and task.ext_designate_outlets:
                        device_code = random.choice(task.ext_designate_outlets)
                        # 若上一个任务为运动节点，设置其终点为本设备
                        if task.pre_process().ext_resource_type.is_movable():
                            task.pre_process().end_pos = device_code
                        # 修正 当前设备信息
                        process.device_type = task.ext_designate_equtype
                        process.device_code = device_code
                        # 修正 开始/结束 设备code
                        process.begin_pos = device_code
                        process.end_pos = device_code
            # 当设置不全时 不做指定修改
            if not process.device_type or not process.device_code:
                process.device_type = device.device_type()
                process.device_code = device.device_code()
            #
            yield self.env.timeout(0 / self.sys_data.time_cost_ratio)
            process.end_ts = self.env.now
            if not process.end_pos:
                process.end_pos = device.loc()
        # 更新 出货量
        if self.device_type in self.sys_data.ch_ports and task.ext_source == TaskSource.DELIVERY:
            self.sys_data.statistical_factory_port[self.device_type][device.device_code()] += 1
        # [交付口隔离] 判断是否有空余 出货口
        self.try_reassign_port(task)

        check_fix_task(task)
        # 若后续还有任务，则推送到相应设备
        if TaskStatus.DONE != task.run_status:
            self.ware_house.distribute_task(task_ctx)

    def try_reassign_port(self, task: Task, error99=False):
        """
        尝试重新分配出货口
        :param task:
        :param error99: 是
        """
        # 发货任务
        if self.device_type in self.sys_data.ch_ports and task.ext_source == TaskSource.DELIVERY:
            self.try_reassign_ch_port(task, error99)
        # 部分在库任务
        elif task.ext_route.command_type_code in self.sys_data.inv_isolated_task:
            self.try_reassign_zk_port(task, error99)

    def try_reassign_ch_port(self, task: Task, error99=False):
        """
        对任务PCS进行扣减, 尝试重新分配出货口  发货任务
        :param task:
        :param error99: 是否错误
        """
        error99_str = '(error99)' if error99 else ''

        factory_code = task.factory_code()
        if not task.ext_costed_pcs:
            task.ext_costed_pcs = True
            self.factory_port_dict[self.device_type][factory_code]['pcs'] -= task.target_sku_num
            self.sys_data.logger.debug(
                f'[PC] 交付口({self.device_type}) {error99_str} vehicle: {factory_code} ({task.task_id}) 交付pcs: {task.target_sku_num}, 剩余pcs: {self.factory_port_dict[self.device_type][factory_code]["pcs"]}')
        if task.is_master_dc_task:
            # 当出现error99时，将所有子任务都置为失败，且错误信息相同
            if error99:
                for otask in task.ext_group_tasks[1:]:
                    otask.ext_last_info = task.ext_last_info
                    otask.run_status = TaskStatus.ERROR
            # 当为 栈板、自制箱、纸箱、直发场景
            if (task.target_pack_type == PackType.ZB.value
                    or (task.origin_pack_type == PackType.ZZX.value and task.target_pack_type == PackType.ZZX.value)
                    or (task.origin_pack_type == PackType.ZX.value and task.target_pack_type == PackType.ZX.value)
                    or Sku.gen_uk(task.sku) in self.sys_data.nosplit_skus):
                for otask in task.ext_group_tasks[1:]:
                    otask: Task
                    if otask.ext_costed_pcs:
                        continue
                    otask.ext_costed_pcs = True
                    self.factory_port_dict[self.device_type][factory_code]['pcs'] -= otask.target_sku_num
                    self.sys_data.logger.debug(
                        f'[PC] 交付口({self.device_type}) {error99_str} ot vehicle: {factory_code} ({otask.task_id}) 交付pcs: {otask.target_sku_num}, 剩余pcs: {self.factory_port_dict[self.device_type][factory_code]["pcs"]}')
        # 若PCS数全部运输完毕，则出货口可空闲出来
        if self.factory_port_dict[self.device_type][factory_code]['pcs'] == 0:
            available_ports = self.factory_port[self.device_type][factory_code]
            self.factory_port[self.device_type][factory_code] = []
            # 重新分配
            prev_factory_port = self.sys_data.prev_factory_port[
                self.device_type] if self.device_type in self.sys_data.prev_factory_port else {}
            factory_port = self.factory_port[self.device_type] if self.device_type in self.factory_port else {}

            if self.assign_port(available_ports, self.factory_port_dict[self.device_type], factory_port,
                                prev_factory_port, self.sys_data.store_layer_dict, self.sys_data.is_designate_outlet,
                                self.sys_data.statistical_factory_port[self.device_type],
                                self.sys_data.urgent_order_add_date_dict, self.device_list, self.sys_data.logger):
                # 回写 口分配
                self.sys_data.factory_port[self.device_type] = self.factory_port[self.device_type]
                # 日志
                self.sys_data.logger.info(
                    f'[PC] 交付口({self.device_type}) 再次分配 {error99_str} time: {ts_str(self.env.now)}'
                    f'\n 可用口数量: {len(self.device_list)} -> {self.device_list} '
                    f'\n 交付对象: {len(self.factory_port_dict[self.device_type])} -> {self.factory_port_dict[self.device_type]}'
                    f'\n 交付口分配: {self.factory_port[self.device_type]}')
        pass

    def try_reassign_zzc_port(self, task: Task, error99=False):
        """
        对任务PCS进行扣减, 尝试重新分配出货口  发货任务(自制)  ---> (代码备份 暂时无用)
        :param task:
        :param error99: 是否错误
        """
        error99_str = '(error99)' if error99 else ''

        factory_code = task.factory_code()
        if not task.ext_costed_pcs:
            task.ext_costed_pcs = True
            self.factory_port_dict[self.device_type][factory_code]['pcs'] -= task.target_sku_num
            self.sys_data.logger.debug(
                f'[PC] 交付口({self.device_type}) {error99_str} factory_code: {factory_code} ({task.task_id}) factory: {factory_code} 交付pcs: {task.target_sku_num}, 剩余pcs: {self.factory_port_dict[self.device_type][factory_code]["pcs"]}')

        if task.is_master_dc_task:
            for otask in task.ext_group_tasks[1:]:
                otask: Task
                factory_code2 = otask.factory_code()
                if factory_code != factory_code2:
                    continue
                if otask.ext_costed_pcs:
                    continue
                otask.ext_costed_pcs = True
                self.factory_port_dict[self.device_type][factory_code]['pcs'] -= otask.target_sku_num
                self.sys_data.logger.debug(
                    f'[PC] 交付口({self.device_type}) {error99_str} ot factory_code: {factory_code} ({otask.task_id}) factory: {factory_code} 交付pcs: {otask.target_sku_num}, 剩余pcs: {self.factory_port_dict[self.device_type][factory_code]["pcs"]}')
                if error99:
                    otask.ext_last_info = task.ext_last_info
                    otask.run_status = TaskStatus.ERROR

        # 若PCS数全部运输完毕，则出货口空闲出来 给其他任务
        if self.factory_port_dict[self.device_type][factory_code]['pcs'] == 0:
            self.factory_port[self.device_type][factory_code] = []
            # 重新分配
            factory_port = self.factory_port[self.device_type] if self.device_type in self.factory_port else {}
            fake_pre_factory = {self.device_type: {}}  # 仅初始分配需要延续
            if self.assign_port(self.device_list, self.factory_port_dict[self.device_type], factory_port,
                                fake_pre_factory, self.sys_data.store_layer_dict, self.sys_data.is_designate_outlet,
                                self.sys_data.statistical_factory_port[self.device_type],
                                self.sys_data.urgent_order_add_date_dict, self.device_list, self.sys_data.logger):
                # 回写 口分配
                self.sys_data.factory_port[self.device_type] = self.factory_port[self.device_type]
                # 日志
                self.sys_data.logger.info(
                    f'[PC] 交付口({self.device_type}) 再次分配 {error99_str} time: {ts_str(self.env.now)}'
                    f'\n 可用口数量: {len(self.device_list)} -> {self.device_list} '
                    f'\n 交付对象: {len(self.factory_port_dict[self.device_type])} -> {self.factory_port_dict[self.device_type]}'
                    f'\n 交付口分配: {self.factory_port[self.device_type]}')

    def try_reassign_zk_port(self, task: Task, error99=False):
        """
        对任务PCS进行扣减, 尝试重新分配出货口  在库任务
        :param task:
        :param error99: 是否错误
        """
        error99_str = '(error99)' if error99 else ''
        if not task.ext_costed_pcs:
            task.ext_costed_pcs = True
            device_code = task.consigneekey
            order_id = task.cwmsinvtaskdetil
            #
            self.zk_factory_port_dict[self.device_type][device_code][order_id]['pcs'] -= task.target_sku_num
            self.sys_data.logger.debug(
                f'[PC] (IQCC) 交付口({self.device_type}) ({device_code}) {error99_str} order: {order_id} ({task.task_id}) device_code: {device_code} 交付pcs: {task.target_sku_num}, 剩余pcs: {self.zk_factory_port_dict[self.device_type][device_code][order_id]["pcs"]}')

            # 板转箱会在提前分任务，不用走该逻辑
            if not (task.origin_pack_type == PackType.ZB.value and not error99):
                if task.is_master_dc_task:
                    for otask in task.ext_group_tasks[1:]:
                        otask: Task
                        if otask.ext_costed_pcs:
                            continue
                        otask.ext_costed_pcs = True
                        device_code = otask.consigneekey
                        order_id = otask.cwmsinvtaskdetil
                        self.zk_factory_port_dict[self.device_type][device_code][order_id][
                            'pcs'] -= otask.target_sku_num
                        self.sys_data.logger.debug(
                            f'[PC] (IQCC) 交付口({self.device_type}) ({device_code}) {error99_str} ot order: {order_id} ({otask.task_id}) device_code: {device_code} 交付pcs: {otask.target_sku_num}, 剩余pcs: {self.zk_factory_port_dict[self.device_type][device_code][order_id]["pcs"]}')
                        if error99:
                            otask.ext_last_info = task.ext_last_info
                            otask.run_status = TaskStatus.ERROR

            # PCS数全部传输完毕，将出口空余出来
            if self.zk_factory_port_dict[self.device_type][device_code][order_id]['pcs'] <= 0:
                if self.zk_factory_port_dict[self.device_type][device_code][order_id]['pcs'] < 0:
                    self.logger.warning(
                        f'[PC] 交付口({self.device_type}) PCS计算异常 出现负数 task: {task.task_id}')
                available_devices = self.zk_factory_port[self.device_type][order_id]
                self.zk_factory_port[self.device_type][order_id] = []
                if self.assign_port_zk(available_devices, self.zk_factory_port_dict[self.device_type],
                                       self.zk_factory_port[self.device_type],
                                       self.sys_data.zk_prev_factory_port[self.device_type], self.sys_data.logger):
                    # 回写
                    self.sys_data.zk_factory_port = self.zk_factory_port
                    # 日志
                    self.sys_data.logger.info(
                        f'[PC] 交付口({self.device_type}) 再次分配 {error99_str} time: {ts_str(self.env.now)}'
                        f'\n 可用口: {len(available_devices)} -> {available_devices} '
                        f'\n 交付车次: {len(self.zk_factory_port_dict)}'
                        f'\n 交付车次及对象: {self.zk_factory_port_dict}'
                        f'\n 交付口分配: {self.zk_factory_port}')


class SystemUnit(System):
    """
    工作单元系统 (除以下4种，其他Unit单元均在此处处理)
    - 201 栈板上下架口
    - 202 栈板出入库口
    - 203 件箱层接驳口
    - 204 件箱上下架接驳口
    """
    unit_group: dict[int, UnitGroup] = {}

    ware_house = None

    # 系统数据上下文
    sys_data: SysDataCtx = None

    def __init__(self, env: Environment, name: str, ware_house, sys_data: SysDataCtx):
        super().__init__(env, name, None)
        self.sys_data = sys_data
        self.ware_house = ware_house
        self.unit_group = {}
        self.__init_unit_group()

    def __init_unit_group(self):
        """
        初始化 工作单元组
        :return:
        """
        # 初始化
        unit_type_dict = {}
        for u in self.sys_data.unit_data:
            dev = UnitDevice(self.env, f'Unit-{u[SysDataCtx.ID_DB_KEY]}', u)
            #
            if not dev.device_type() in unit_type_dict:
                unit_type_dict[dev.device_type()] = []
            if self.sys_data.is_valid_equ(dev):
                unit_type_dict[dev.device_type()].append(dev)
        #
        for k, v in unit_type_dict.items():
            ug = UnitGroup(self.env, f'UnitGroup-{k}', k, self.ware_house, v, self.sys_data)
            self.unit_group[k] = ug

    def fix_reassign_port_when_error(self, task: Task):
        group: UnitGroup | None = None
        # 设备为 (发货)出货口类
        if task.ext_route.ext_ch_port:
            device_type = task.ext_route.ext_ch_port
            if device_type in self.unit_group:
                group = self.unit_group[device_type]
        elif task.ext_route.command_type_code in self.sys_data.inv_isolated_task:
            if task.ext_zk_equ_type in self.unit_group:
                group = self.unit_group[task.ext_zk_equ_type]
        if group:
            group.try_reassign_port(task, True)

    def run_task(self, task_ctx: TaskCtx):
        task = task_ctx.task
        process = task.process
        device_type = process.ext_step.ext_equ_type
        # 仅Center-GTP存在多设备类型情况
        if not (device_type and device_type[0] in self.unit_group):
            self.logger.error((
                f'[PC] 设备类型未知? Unit equ_type: {device_type}, task: {task.task_id}, '
                f'route: {task.ext_route.name}({task.ext_route.id}), seq: {process.ext_step.seq}'))
            return
        device_type = device_type[0]
        # 部分任务 不具备执行条件, 直接设置任务为失败状态
        if self.sys_data.is_designate_outlet:
            # 有指定不同层设备类型 但指定口不可用
            if task.ext_designate_equtype and not task.ext_designate_outlets:
                #
                task.ext_last_info = f'跨层指定出口，且指定口不可用'
                task.set_run_status(TaskStatus.ERROR, self.sys_data)
                return
        ug = self.unit_group.get(device_type)
        if ug:
            ug.push_task(task_ctx)
        else:
            msg = f'[PC] Unit equ_type: {device_type} 不存在'
            self.sys_data.logger.error(msg)
        yield self.env.timeout(0 / self.sys_data.time_cost_ratio)

    def start(self):
        super().start()
        for ug in self.unit_group.values():
            ug.start()

    def stop(self):
        for ug in self.unit_group.values():
            ug.stop()
        super().stop()
